<?php

/*
 * Адаптер к базе данных MySQL для моделей данных
 */

namespace T4\MODEL;

use T4\Exception;

class MysqlAdapter extends AAdapter {
    
    const DEFAULT_ENGINE = 'InnoDB';

    /**
     * Развертывание и удаление моделей
     */
	
    public function getColumnType($definition) {

		if (isset($definition['absolutetype']) && !empty($definition['absolutetype']))
			return $definition['absolutetype'];

		switch ($definition['type']) {

			case 'link':
				return 'INT UNSIGNED NOT NULL DEFAULT \'0\'';
				break;

			case 'date':
				return 'DATE';
				break;

			case 'time':
				return 'TIME';
				break;

			case 'datetime':
				return 'DATETIME';
				break;

			case 'string':
				if (!isset($definition['length']) || empty($definition['length']))
					return 'VARCHAR(255)';
				else
					return 'VARCHAR(' . intval($definition['length']) . ')';
				break;

			case 'text':
				return 'TEXT';
				break;

			case 'file':
			case 'image':
				return 'VARCHAR(255)';
				break;

			case 'int':
				if (!isset($definition['length']) || empty($definition['length']))
					return 'INT';
				else
					switch ($definition['length']) {
						case 1:
						case 'tiny':
							return 'TINYINT';
							break;
						case 2:
						case 'small':
							return 'SMALLINT';
							break;
						case 3:
						case 'medium':
							return 'MEDIUMINT';
							break;
						case 4:
							return 'INT';
							break;
						case 8:
						case 'big':
							return 'BIGINT';
							break;
						default:
							return 'INT';
							break;
					}
		}
	}
	
	protected function getIndexDDL($indexDefinition) {
		
		$columns = '`' . implode('`,`', (array) $indexDefinition['columns']) . '`';
		
		if (empty($indexDefinition['name'])) {
			$name = str_replace(array(',', '`'), array('_', ''), $columns);
		} else {
			$name = $indexDefinition['name'];
		}
		
		if (empty($indexDefinition['type'])) {
			$type = 'INDEX';
		} else {
			$type = strtoupper($indexDefinition['type']);
		}
		
		return $type . ' `' . $name . '` (' . $columns . ')';
		
	}

	public function install() {

        $schema = self::getSchema();

        if (!isset($schema['columns']) || empty($schema['columns']))
            throw new Exception('Невозможно создать таблицу ' . self::getTable() . ': в схеме данных отсутствует информация о полях таблицы');

        /**
         * Поля и индексы в таблице
         * Поле с именем id игнорируется и добавляется автоматически, 
         * только если не задан явно absolutetype
         * (нерекомендуемая ситуация)
         */
        $sqlColumns = array();
        $sqlIndexes = array();

        $forcePK = false;

        foreach ($schema['columns'] as $name => $definition) {

            if ($name == 'id') {
                if (empty($definition['absolutetype'])) {
                    continue;
                } else {
                    $sqlIndexes[] = 'PRIMARY KEY(`id`)';
                    $forcePK = true;
                }
            }

            $sqlColumns[] =
                    '`' . $name . '` ' . self::getColumnType($definition) .
                    (!empty($definition['comment']) ? ' COMMENT \'' . $definition['comment'] . '\'' : '' );
        }

        if (!$forcePK) {
            array_unshift($sqlColumns, "`id` INT UNSIGNED NOT NULL AUTO_INCREMENT");
            array_unshift($sqlIndexes, "PRIMARY KEY `id` (`id`)");
        }

        foreach ($schema['indexes'] as $index) {
			$sqlIndexes[] = $this->getIndexDDL($index);
        }

        /**
         * Связи. 
		 * Поля и индексы для связей
         */
        if (!empty($schema['relations'])) {
            foreach ($schema['relations'] as $name => $relation) {				
				$relation = RelationFactory::getInstance($this, $name, $relation);
				$sqlColumns[] = $relation->getRelationColumnDDL();
				$sqlIndexes[] = $relation->getRelationIndexDDL();
            }
        }

        $sql = "
				CREATE TABLE " . self::getTable() . "(
				" . implode(",\n", $sqlColumns) . "
				" . (!empty($sqlIndexes) ? ",\n" . implode(",\n", $sqlIndexes) : '' ) . "
				)
				ENGINE=" . (!empty($schema['engine']) ? $schema['engine'] : self::DEFAULT_ENGINE ) . "
				;
		";

        $result = \T4\DB::getInstance()
                ->prepare($sql)
                ->execute();

        /*
         * Секция data - Данные в таблицу
         */
        if (!empty($schema['data'])) {

            $fields = array_keys($schema['columns']);
            $values = array_map(
                    function ($x) {
                        return ':' . $x;
                    }, $fields
            );

            foreach ($schema['data'] as $row) {

                $sql = '
                    INSERT INTO ' . self::getTable() . '
                    (`' . implode('`, `', $fields) . '`) VALUES (' . implode(', ', $values) . ')
                ';

                $prepared = array();
                foreach ($row as $key => $value)
                    $prepared[':' . $fields[$key]] = $value;

                $db = \T4\DB::getInstance();
                $result = $db
                        ->prepare($sql)
                        ->execute($prepared);

            }
            
        }

    }

    public function uninstall() {

        $sql = 'DROP TABLE `' . self::getTable() . '`';

        $result = \T4\DB::getInstance()
                ->prepare($sql)
                ->execute();

        /**
         * @todo Не вижу ошибок!!!
         */
        if (false === $result) {
            $error = $result->errorInfo();
            throw new Exception('Ошибка при удалении таблицы ' . self::getTable() . ': ' . $error[2]);
        }
        
    }

    /**
     * Подготовка запроса на выборку к базе с учетом всех связей
     */
    
    public function prepareColumnList() {
        $table = $this->getTable();
        $class = $this->modelClass;
        $columns = array();
        foreach ( $class::getSchemaColumns() as $column ) {
            $columns[] = '`' . $table . '`.`' . $column . '` AS `' . $table. '__' . $column . '`';
        }
        return $columns;
    }
	
	protected function getColumnAliases() {
        $table = $this->getTable();
        $class = $this->modelClass;
        $columns = array();
        foreach ( $class::getSchemaColumns() as $column ) {
            $columns[$column] = $table. '__' . $column;
        }
        return $columns;		
	}
	
	protected function getColumnAlias($column) {
		$aliases = $this->getColumnAliases();
		return $aliases[$column];
	}
    
    protected function prepareSelectSQL($where = null, $order = null, $limit = null) {

        /**
         * Заполняем массивы - 
         * какие таблицы будут участвовать в запросе и какие в них поля
         */
        $tables[] = '`' . ( $table = $this->getTable() ) . '`';
        $columns = $this->prepareColumnList();
        
        /**
         * Связи
         */
        $schema = $this->getSchema();
        if (!empty($schema['relations'])) {
            foreach ($schema['relations'] as $name => $relation) {				
				$relation = RelationFactory::getInstance($this, $name, $relation);
				$tables[] = $relation->getTablesJoin();
				$columns = array_merge($columns, $relation->getColumnsList());
            }
        }
		
		/**
		 * Обрабатываем order 
		 */
		if ( !is_null($order) ) {
			$orderParts = explode(',', $order);
			$aliases = $this->getColumnAliases();
			foreach ( $orderParts as &$part ) {
				preg_match('/(\S+)\s*(ASC|DESC)?/i', $part, $matches);
				if ( !isset($matches[2]) )
					$matches[2] = 'ASC';
				$matches[2] = strtoupper($matches[2]);
				$part = '`' . $aliases[$matches[1]] . '` ' . $matches[2];
			}
			$order = implode(',', $orderParts);
		}

        $sql = '
SELECT ' . implode(', ', $columns) . '
    FROM ' . implode(' ', $tables) . '
WHERE 1' . ( is_null($where) ? '' : ' AND (' . $where . ')' ) . '
    ' . ( is_null($order) ? '' : ' ORDER BY ' . $order . '' ) . '
    ' . ( is_null($limit) ? '' : ' LIMIT ' . $limit . '' ) . '
        ';

		return $sql;
        
    }

    /**
     * Обработка "сырых" данных и их трансформация в экземпляр модели
     * @param type $data "Сырые" данные
     * @return \T4\Model Экземпляр модели 
     */
    public function makeObject($data) {
        
        $table = $this->getTable();
        
        /**
         * "Собственные" поля модели 
         */
        $selfData = array();
        foreach ( $data as $column => $value ) {
            if ( 0 === strpos($column, $table.'__') ) {
                $selfData[str_replace($table.'__', '', $column)] = $value;
            }
        }
        
        $class = $this->modelClass;
        $object = new $class($selfData);
        
        /**
         * Отношения 
         */
        if ( $class::hasSchemaRelations() ) {
            
            $schema = $this->getSchema();
            foreach ($schema['relations'] as $name => $relation) {

                switch ($relation['type']) {
                    case 'many-to-one':
                    default:
                        $className = $relation['model'];
                        $adapter = AdapterCollection::getInstance($className);
                        $object->$name = $adapter->makeObject($data);
                }
            }
            
        }

        return $object;
        
    }
    
    public function query($query) {
        
        $data = \T4\DB::getInstance()
                ->prepare($query)
                ->execute()
                ->fetchAll(\T4\DB\Connection::FETCH_ASSOC);

        $collection = new Collection;
        if (!empty($data)) {
            foreach ($data as &$row)
                $collection[] = $this->makeObject($row);
        }

        return $collection;

    }

    /**
     * ----------------------------------------
     * Статические (в модели) методы (вся таблица)
     * ----------------------------------------
     */
    
    public function findAll($order = null) {

        $sql = $this->prepareSelectSQL(null, $order);

        $data = \T4\DB::getInstance()
                ->prepare($sql)
                ->execute()
                ->fetchAll(\T4\DB\Connection::FETCH_ASSOC);

        $collection = new Collection;
        if (!empty($data)) {
            foreach ($data as &$row)
                $collection[] = $this->makeObject($row);
        }

        return $collection;
    }

    public function findByField($field, $value, $where = null, $order = null, $limit = null) {

        $sql = $this->prepareSelectSQL(
                '`' . $field . '`=:value' . ( is_null($where) ? '' : ' AND (' . $where . ')' ), $order, $limit
        );

        $db = \T4\DB::getInstance();

        $data = $db
                ->prepare($sql)
                ->execute(array(':value' => $value))
                ->fetchAll(\T4\DB\Connection::FETCH_ASSOC);

        $collection = new Collection;
        if (!empty($data)) {
            foreach ($data as &$row)
                $collection[] = $this->makeObject($row);
        }

        return $collection;
    }

    public function findOne($where = null, $order = null) {

        $sql = $this->prepareSelectSQL(
                $where, $order, 1
        );

        $data = \T4\DB::getInstance()
                ->prepare($sql)
                ->execute(array(':value' => $value))
                ->fetch(\T4\DB\Connection::FETCH_ASSOC);

        if (!empty($data)) {
            $data = $this->makeObject($data);
        }

        return $data;
    }

    public function findOneByField($field, $value, $where = null, $order = null) {

        $sql = $this->prepareSelectSQL(
                '`' . $this->getTable() . '`.`' . $field . '`=:value' . ( is_null($where) ? '' : ' AND (' . $where . ')' ), $order, 1
        );

        $data = \T4\DB::getInstance()
                ->prepare($sql)
                ->execute(array(':value' => $value))
                ->fetch(\T4\DB\Connection::FETCH_ASSOC);

        if (!empty($data)) {
            $data = $this->makeObject($data);
        }

        return $data;
    }

    /*
     * Динамические (в модели) методы (одна запись)
     */

    public function insert() {

        $class = $this->modelClass;

        $data = $this->model->getData();

        $sql = 'INSERT INTO `' . self::getTable() . '`';

        $fields = array();
        $values = array();
        $prepared = array();
        foreach ($data as $field => $value) {
            $fields[] = '`' . $field . '`';
            $values[] = ':' . $field . '';
            $prepared[':' . $field] = $value;
        };

        $sql .= ' (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $values) . ')';

        $db = \T4\DB::getInstance();
        $result = $db
                ->prepare($sql)
                ->execute($prepared);

        /**
         * @todo Не вижу ошибку при ошибке в имени поля!
         */
        if (false === $result) {
            $error = $result->errorInfo();
            throw new Exception('Ошибка при обновлении записи в SQL: ' . $error[2]);
        }

        $this->model->{$class::PRIMARY_KEY} = $db->lastInsertId();
    }

    public function update() {

        $class = $this->modelClass;

        $id = $this->model->{$class::PRIMARY_KEY};
        $data = $this->model->getData();
        unset($data[$class::PRIMARY_KEY]);

        $sql = 'UPDATE `' . self::getTable() . '`';

        $update = array();
        $prepared = array();
        foreach ($data as $field => $value) {
            $update[] = '`' . $field . '` = :' . $field;
            $prepared[':' . $field] = $value;
        };

        $sql .= ' SET ' . implode(', ', $update) . ' WHERE `' . $class::PRIMARY_KEY . '`=:id';

        $db = \T4\DB::getInstance();
        $result = $db
                ->prepare($sql)
                ->execute(
                array_merge(
                        array(':id' => $id), $prepared
                )
        );

        /**
         * @todo Не вижу ошибку при ошибке в имени поля!
         */
        if (false === $result) {
            $error = $result->errorInfo();
            throw new Exception('Ошибка при обновлении записи в SQL: ' . $error[2]);
        }
    }

    public function delete() {

        if (empty($this->model))
            throw new Exception('Адаптеру не передана модель!');

        $class = $this->modelClass;

        if (!$this->model->__isset($class::PRIMARY_KEY))
            throw new Exception('В записи не найдено значение первичного ключа, удаление невозможно!');

        $sql = '
			DELETE
			FROM `' . self::getTable() . '`
			WHERE `' . $class::PRIMARY_KEY . '`=:id
		';

        $result = \T4\DB::getInstance()
                ->prepare($sql)
                ->execute(array(':id' => $this->model->{$class::PRIMARY_KEY}));

        /**
         * @todo Не вижу ошибок!!!
         */
        if (false === $result) {
            $error = $result->errorInfo();
            throw new Exception('Ошибка при обновлении записи в SQL: ' . $error[2]);
        }
    }

}